package de.hauschild.gmltracer.tracer.impl.tier1;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.math.geometry.Vector3D;

import de.hauschild.gmltracer.tracer.Intersection;
import de.hauschild.gmltracer.tracer.Ray;
import de.hauschild.gmltracer.tracer.Shape;
import de.hauschild.gmltracer.tracer.SurfaceFunction;
import de.hauschild.gmltracer.tracer.impl.AbstractShape;

/**
 * This implementation of <code>union</code> has the following improvement to the default implementation {@link Union}.
 * <p>
 * With default {@link Union} the scene will result in a tree where the inner nodes are {@link Union Unions} and the
 * leafs are concrete {@link Shape} implementations. There is no good way to optimize the 'unioned' scene for
 * intersection tests.
 * </p>
 * <p>
 * Here is the improvement: this implementation takes like specification two other {@link Shape Shapes}. If one of this
 * given {@link Shape} is instance of {@link CollectiveUnion} its shape collection will copied to its own shape
 * collection. Doing so a scene build by <code>union</code> will result in one instance of {@link CollectiveUnion} with
 * all scene {@link Shape Shapes} in its shape collection. Now there is space to optimize the shape collection for later
 * intersection tests.
 * </p>
 * 
 * @since 1.0.0
 * @author Klaus Hauschild
 */
public class CollectiveUnion extends AbstractShape {

  private static final SurfaceFunction NOP_SUREFACE_FUNCTION = new NopSurfaceFunction();
  private static final IntersectionDistanceComparator INTERSECTION_COMPARATOR = new IntersectionDistanceComparator();

  private final List<Shape> shapes = new ArrayList<Shape>();
  private AbstractShape lastIntersected;

  /**
   * Instantiates a new {@link CollectiveUnion}.
   * 
   * @param theFirst
   *          the first shape
   * @param theSecond
   *          the second shape
   */
  public CollectiveUnion(final Shape theFirst, final Shape theSecond) {
    super(NOP_SUREFACE_FUNCTION);
    collect(theFirst);
    collect(theSecond);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public SurfaceFunction getSurfaceFunction() {
    if (lastIntersected != null) {
      return lastIntersected.getSurfaceFunction();
    }
    return NOP_SUREFACE_FUNCTION;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Vector3D objectCoordinates(final Vector3D theIntersection) {
    if (lastIntersected != null) {
      return lastIntersected.objectCoordinates(theIntersection);
    }
    return null;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void rotateX(final double degrees) {
    for (final Shape shape : shapes) {
      shape.rotateX(degrees);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void rotateY(final double degrees) {
    for (final Shape shape : shapes) {
      shape.rotateY(degrees);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void rotateZ(final double degrees) {
    for (final Shape shape : shapes) {
      shape.rotateZ(degrees);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void scale(final double x, final double y, final double z) {
    for (final Shape shape : shapes) {
      shape.scale(x, y, z);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void translate(final double x, final double y, final double z) {
    for (final Shape shape : shapes) {
      shape.translate(x, y, z);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void uniformScale(final double scale) {
    for (final Shape shape : shapes) {
      shape.uniformScale(scale);
    }
  }

  /**
   * Returns the shapes.
   * 
   * @return the shapes
   */
  protected List<Shape> getShapes() {
    return shapes;
  }

  @Override
  protected Intersection intersectAfterIgnoration(final Ray ray, final List<Shape> shapesToIngore) {
    final List<Intersection> possibleIntersections = new ArrayList<Intersection>();
    // test each shape for intersection
    for (final Shape shape : shapes) {
      final Intersection intersection = shape.intersect(ray, shapesToIngore);
      if (intersection != null) {
        possibleIntersections.add(intersection);
      }
    }
    // no intersection found
    if (possibleIntersections.isEmpty()) {
      lastIntersected = null;
      return null;
    }
    // only one intersection -> this was it
    if (possibleIntersections.size() == 1) {
      lastIntersected = (AbstractShape) possibleIntersections.get(0).getShape();
      return possibleIntersections.get(0);
    }
    // more than one possible intersection -> sort them by distance to viewer
    Collections.sort(possibleIntersections, INTERSECTION_COMPARATOR);
    // the first is the nearest
    lastIntersected = (AbstractShape) possibleIntersections.get(0).getShape();
    return possibleIntersections.get(0);
  }

  /**
   * If the shape is instance of {@link CollectiveUnion} its shape collection will be copied to this shape collection.
   * For all other shape implementation it will be simply copied into this shape collection.
   * 
   * @param shape
   *          the shape
   */
  private void collect(final Shape shape) {
    if (shape instanceof CollectiveUnion) {
      shapes.addAll(((CollectiveUnion) shape).getShapes());
    } else {
      shapes.add(shape);
    }
  }
}
